package nl.utwente.eemcs.common.analysis;

import java.util.ArrayList;
import java.util.List;

import nl.utwente.eemcs.common.helpers.GraphBuilder;
import nl.utwente.eemcs.graph.*;

/**
 * Class for replacing composite nodes
 * @author Evert Duipmans
 */
public class CompositeNodeBlockAnalyzer {
	/**
	 * Graph builder in which all the process information is kept
	 */
	private GraphBuilder builder;
	
	/**
	 * Create a composite node analyzer instance
	 * @param builder Graph builder
	 */
	private CompositeNodeBlockAnalyzer(GraphBuilder builder) {
		this.builder = builder;
	}
	
	/**
	 * Analyze a process and replace composite constructs, by composite nodes
	 * @param builder Graph builder
	 * @throws AnalysisException When an error occurs during the process, an Analysis Exception is thrown
	 */
	public static void analyze(GraphBuilder builder) throws AnalysisException {
		CompositeNodeBlockAnalyzer analyzer = new CompositeNodeBlockAnalyzer(builder);
		analyzer.analyze();
	}
	
	/**
	 * Analyze the process
	 * @throws AnalysisException When an error occurs during the process, an Analysis Exception is thrown
	 */
	public void analyze() throws AnalysisException {
		Graph g = builder.getGraph();
		Node startNode = findStartNode(g);
		Graph outputGraph = new Graph();
		
		// Replace the composite start and end nodes by composite nodes
		replaceCompositeNodes(startNode, outputGraph, g, new ArrayList<Node>());
		
		// Replace graph
		builder.setGraph(outputGraph);
	}

	/**
	 * Replace start and end nodes of parallel and conditional constructs by composite nodes
	 * @param currentNode
	 * @param newGraph
	 * @param lookupGraph
	 * @param visitedNodes
	 * @return
	 * @throws AnalysisException
	 */
	private Node replaceCompositeNodes(Node currentNode, Graph newGraph, Graph lookupGraph, List<Node> visitedNodes) throws AnalysisException {
		// When the node is visited, return
		if (visitedNodes.contains(currentNode))
			return null;
		
		// Actions for specific node types
		if (currentNode instanceof ConditionalStartNode) {
			currentNode = replaceConditionalNodes((ConditionalStartNode) currentNode, newGraph, lookupGraph, visitedNodes);
		} else if (currentNode instanceof ParallelStartNode) {
			currentNode = replaceParallelNodes((ParallelStartNode) currentNode, newGraph, lookupGraph, visitedNodes);
		} else if (currentNode instanceof ParallelEndNode || currentNode instanceof ConditionalEndNode) {
			return currentNode;
		} else {
			// Add node to new graph
			newGraph.addNode(currentNode);
			
			// Add current node to visited nodes
			visitedNodes.add(currentNode);
		}
		
		// Get outgoing edges
		List<Edge> outgoingEdges = lookupGraph.getOutgoingEdges(currentNode, EdgeType.Control);
		
		Node result = null;
		for (Edge e : outgoingEdges) {
			newGraph.addEdge(e);
			Node interMedResult = replaceCompositeNodes(e.getEnd(), newGraph, lookupGraph, visitedNodes);
			if (interMedResult != null)
				result = interMedResult;
		}
		return result;
	}	
	
	private ConditionalConstruct replaceConditionalNodes(ConditionalStartNode currentNode, Graph newGraph, Graph lookupGraph, List<Node> visitedNodes) throws AnalysisException {
		ConditionalConstruct construct = new ConditionalConstruct(currentNode.getName() + "_comp");
		construct.setStartNode(currentNode);
		currentNode.setParentConstruct(construct);
		
		Node endNode = null;
		
		// Add to graph and replace outgoing and incoming edges
		for (Edge e : lookupGraph.getOutgoingEdges(currentNode, EdgeType.Control)) {
			Graph branch = new Graph();
			
			// Collect branch and find end node
			endNode = replaceCompositeNodes(e.getEnd(), branch, lookupGraph, visitedNodes);
			
			// Set branch
			boolean branchType = (e.getLabel().equalsIgnoreCase("true") || construct.getBranch(false) != null);
			construct.setBranch(branch, branchType);
			
			// Add label to start node
			((ConditionalStartNode)currentNode).addAttribute(branchType + "Branch", e.getLabel());

			// Remove edges from last activity to the end node
			for (Edge edge : branch.getIncomingEdges(endNode, EdgeType.Control))
				branch.removeEdge(edge);
		}
		
		// If the end node is invalid, throw exception
		if (endNode == null)
			throw new AnalysisException("No end node found for node "+currentNode.getName()+", conditional end node expected.");
		else if (!(endNode instanceof ConditionalEndNode))
			throw new AnalysisException("Invalid end node found for node "+currentNode.getName()+", conditional end node expected, "+endNode.getName()+" found.");
		else {
			// Set end node
			construct.setEndNode((ConditionalEndNode)endNode);
			((ConditionalEndNode)endNode).setParentConstruct(construct);
			newGraph.addNode(construct);
			
			// Replace edges to startConditionalNode
			for (Edge e : lookupGraph.getIncomingEdges(currentNode, EdgeType.Control))
				e.setEnd(construct);
			
			// Replace edges from endConditionalNode to next nodes
			List<Edge> outgoingEdges = lookupGraph.getOutgoingEdges(endNode, EdgeType.Control);
			for (Edge e : outgoingEdges) {
				e.setStart(construct);
				if (visitedNodes.contains(e.getEnd()))
					newGraph.addEdge(e);	
			}
			
			// Add end node to visited nodes
			visitedNodes.add(endNode);
		}
		return construct;
	}
	
	private ParallelConstruct replaceParallelNodes(ParallelStartNode currentNode, Graph newGraph, Graph lookupGraph, List<Node> visitedNodes) throws AnalysisException {
		ParallelConstruct construct = new ParallelConstruct(currentNode.getName() + "_comp");
		construct.setStartNode(currentNode);
		currentNode.setParentConstruct(construct);
		
		Node endNode = null;
		
		// Add to graph and replace outgoing and incoming edges
		for (Edge e : lookupGraph.getOutgoingEdges(currentNode, EdgeType.Control)) {
			Graph branch = new Graph();
			
			// Collect branch and find end node
			endNode = replaceCompositeNodes(e.getEnd(), branch, lookupGraph, visitedNodes);
			
			// Set branch
			construct.addBranch(branch);
			
			// Remove edges from last activity to the end node
			for (Edge edge : branch.getIncomingEdges(endNode, EdgeType.Control))
				branch.removeEdge(edge);				
		}
		
		// If the end node is invalid, throw exception		
		if (endNode == null)
			throw new AnalysisException("No end node found for node "+currentNode.getName()+", parallel end node expected");
		else if (!(endNode instanceof ParallelEndNode))
			throw new AnalysisException("Invalid end node found for node "+currentNode.getName()+", parallel end node expected, "+endNode.getName()+" found.");
		else {
			// Set end node
			construct.setEndNode((ParallelEndNode)endNode);
			((ParallelEndNode)endNode).setParentConstruct(construct);
			newGraph.addNode(construct);
			
			// Replace edges to startConditionalNode
			for (Edge e : lookupGraph.getIncomingEdges(currentNode, EdgeType.Control))
				e.setEnd(construct);
			
			// Replace edges from endConditionalNode to next nodes
			List<Edge> outgoingEdges = lookupGraph.getOutgoingEdges(endNode, EdgeType.Control);
			for (Edge e : outgoingEdges) {
				e.setStart(construct);
				
				if (visitedNodes.contains(e.getEnd()))
					newGraph.addEdge(e);
			}
			
			// Add end node to visited nodes
			visitedNodes.add(endNode);	
		}
		return construct;
	}	
	
	/**
	 * Find the start node of a graph
	 * @param g Graph
	 * @return Start node
	 * @throws AnalysisException When no start node is found or multiple start nodes are found, an exception is thrown
	 */
	private Node findStartNode(Graph g) throws AnalysisException {
		Node result = null;
		int counter = 0;
		
		// Find start nodes
		for (Node n : g.getNodes()) {
			if (n.hasAttribute("type") && ((String)n.getAttribute("type")).equals("Trigger")) {
				counter++;
				result = n;
			}
		}
		
		if (counter == 0)
			throw new AnalysisException("No start node found");
		else if (counter > 1) 
			throw new AnalysisException("Multiple start nodes found");
		else
			return result;
	}
}
